<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="keywords" content="Hexo Theme Keep">
    <meta name="description" content="Hexo Theme Keep">
    <meta name="author" content="Kaiser">
    
    <title>
        
            单点登录的三种实现方式 |
        
        Kaiser
    </title>
    <link rel="shortcut icon" href="/kaisering.github.io/images/logo.svg">
    
<link rel="stylesheet" href="/kaisering.github.io/css/style.css">

    
<link rel="stylesheet" href="/kaisering.github.io/css/font-awesome.min.css">

    <script id="hexo-configurations">
    let KEEP = window.KEEP || {};
    KEEP.hexo_config = {"hostname":"github.com","root":"/kaisering.github.io/","path":"search.json"};
    KEEP.theme_config = {"toc":{"enable":false,"number":false,"expand_all":false},"style":{"primary_color":"#0066CC","avatar":"/images/avatar.svg","favicon":"/images/logo.svg","article_img_align":"left","left_side_width":"260px","content_max_width":"920px","hover":{"shadow":false,"scale":false},"first_screen":{"enable":true,"background_img":"/images/bg.svg","description":"Keep writing and Keep loving."},"scroll":{"progress_bar":{"enable":false},"percent":{"enable":false}}},"local_search":{"enable":true,"trigger":"auto","unescape":false,"preload":true},"code_copy":{"enable":false,"style":"default"},"pjax":{"enable":false},"lazyload":{"enable":false},"version":"3.2.0"};
    KEEP.language_ago = {"second":"%s seconds ago","minute":"%s minutes ago","hour":"%s hours ago","day":"%s days age","week":"%s weeks age","month":"%s months age","year":"%s years age"};
  </script>
<meta name="generator" content="Hexo 5.3.0"><link rel="alternate" href="/kaisering.github.io/atom.xml" title="Hexo" type="application/atom+xml">
</head>


<body>
<div class="progress-bar-container">
    

    
</div>


<main class="page-container">

    

    <div class="page-main-content">

        <div class="page-main-content-top">
            <header class="header-wrapper">

    <div class="header-content">
        <div class="left">
            <a class="logo-title" href="/">
                Kaiser
            </a>
        </div>

        <div class="right">
            <div class="pc">
                <ul class="menu-list">
                    
                        <li class="menu-item">
                            <a class=""
                               href="/kaisering.github.io/"
                            >
                                HOME
                            </a>
                        </li>
                    
                        <li class="menu-item">
                            <a class=""
                               href="/kaisering.github.io/archives"
                            >
                                ARCHIVES
                            </a>
                        </li>
                    
                        <li class="menu-item">
                            <a class=""
                               href="/kaisering.github.io/categories"
                            >
                                CATEGORIES
                            </a>
                        </li>
                    
                        <li class="menu-item">
                            <a class=""
                               href="/kaisering.github.io/tags"
                            >
                                TAGS
                            </a>
                        </li>
                    
                        <li class="menu-item">
                            <a class=""
                               href="/kaisering.github.io/links"
                            >
                                LINKS
                            </a>
                        </li>
                    
                        <li class="menu-item">
                            <a class=""
                               href="/kaisering.github.io/about"
                            >
                                ABOUT
                            </a>
                        </li>
                    
                    
                        <li class="menu-item search search-popup-trigger">
                            <i class="fas fa-search"></i>
                        </li>
                    
                </ul>
            </div>
            <div class="mobile">
                
                    <div class="icon-item search search-popup-trigger"><i class="fas fa-search"></i></div>
                
                <div class="icon-item menu-bar">
                    <div class="menu-bar-middle"></div>
                </div>
            </div>
        </div>
    </div>

    <div class="header-drawer">
        <ul class="drawer-menu-list">
            
                <li class="drawer-menu-item flex-center">
                    <a class=""
                       href="/kaisering.github.io/">HOME</a>
                </li>
            
                <li class="drawer-menu-item flex-center">
                    <a class=""
                       href="/kaisering.github.io/archives">ARCHIVES</a>
                </li>
            
                <li class="drawer-menu-item flex-center">
                    <a class=""
                       href="/kaisering.github.io/categories">CATEGORIES</a>
                </li>
            
                <li class="drawer-menu-item flex-center">
                    <a class=""
                       href="/kaisering.github.io/tags">TAGS</a>
                </li>
            
                <li class="drawer-menu-item flex-center">
                    <a class=""
                       href="/kaisering.github.io/links">LINKS</a>
                </li>
            
                <li class="drawer-menu-item flex-center">
                    <a class=""
                       href="/kaisering.github.io/about">ABOUT</a>
                </li>
            
        </ul>
    </div>

    <div class="window-mask"></div>

</header>


        </div>

        <div class="page-main-content-middle">

            <div class="main-content normal-code-theme">

                
                    <div class="fade-in-down-animation">
    <div class="article-content-container">

        <div class="article-title">
            <span class="title-hover-animation">单点登录的三种实现方式</span>
        </div>

        
            <div class="article-header">
                <div class="avatar">
                    <img src="/kaisering.github.io/images/avatar.svg">
                </div>
                <div class="info">
                    <div class="author">
                        <span>Kaiser</span>
                        <span class="level">Lv1</span>
                    </div>
                    <div class="meta-info">
                        <div class="article-meta-info">
    <span class="article-date article-meta-item">
        <i class="fas fa-calendar"></i> 2021-01-18 19:10:47
    </span>
    
    

    
    
    
    
</div>

                    </div>
                </div>
            </div>
        

        <div class="article-content markdown-body">
            <p><a href="#0">前言</a></p>
<p>实现方式一：<a href="#1">父域 Cookie</a></p>
<p>实现方式二：<a href="#2">认证中心</a></p>
<p>实现方式三：<a href="#3">LocalStorage 跨域</a></p>
<p>补充：<a href="#4">域名分级</a></p>
<h3 id="前言"><a href="#前言" class="headerlink" title=" 前言 "></a><span id = "0"> 前言 </span></h3><p>在 B/S 系统中，登录功能通常都是基于 Cookie 来实现的。当用户登录成功后，一般会将登录状态记录到 Session 中，或者是给用户签发一个 Token，无论哪一种方式，都需要在客户端保存一些信息（Session ID 或 Token ），并要求客户端在之后的每次请求中携带它们。</p>
<p>在这样的场景下，使用 Cookie 无疑是最方便的，因此我们一般都会将 Session 的 ID 或 Token 保存到 Cookie 中，当服务端收到请求后，通过验证 Cookie 中的信息来判断用户是否登录 。</p>
<p>单点登录（Single Sign On, SSO）是指在同一帐号平台下的多个应用系统中，用户只需登录一次，即可访问所有相互信任的应用系统。</p>
<p>举例来说，百度贴吧和百度地图是百度公司旗下的两个不同的应用系统，如果用户在百度贴吧登录过之后，当他访问百度地图时无需再次登录，那么就说明百度贴吧和百度地图之间实现了单点登录。</p>
<p>单点登录的本质就是在多个应用系统中共享登录状态。如果用户的登录状态是记录在 Session 中的，要实现共享登录状态，就要先共享 Session，比如可以将 Session 序列化到 Redis 中，让多个应用系统共享同一个 Redis，直接读取 Redis 来获取 Session。</p>
<p>当然仅此是不够的，因为不同的应用系统有着不同的域名，尽管 Session 共享了，但是由于 Session ID 是往往保存在浏览器 Cookie 中的，因此存在作用域的限制，无法跨域名传递，也就是说当用户在 app1.com 中登录后，Session ID 仅在浏览器访问 app1.com 时才会自动在请求头中携带，而当浏览器访问 app2.com 时，Session ID 是不会被带过去的。实现单点登录的关键在于，如何让 Session ID（或 Token）在多个域中共享。</p>
<h3 id="实现方式一：父域-Cookie"><a href="#实现方式一：父域-Cookie" class="headerlink" title=" 实现方式一：父域 Cookie "></a><span id = "1"> 实现方式一：父域 Cookie </span></h3><p>在将具体实现之前，我们先来聊一聊 Cookie 的作用域。</p>
<p>Cookie 的作用域由 domain 属性和 path 属性共同决定。domain 属性的有效值为当前域或其父域的域名/IP地址，在 Tomcat 中，domain 属性默认为当前域的域名/IP地址。path 属性的有效值是以“/”开头的路径，在 Tomcat 中，path 属性默认为当前 Web 应用的上下文路径。</p>
<p>如果将 Cookie 的 domain 属性设置为当前域的父域，那么就认为它是父域 Cookie。Cookie 有一个特点，即父域中的 Cookie 被子域所共享，换言之，子域会自动继承父域中的Cookie。</p>
<p>利用 Cookie 的这个特点，不难想到，将 Session ID（或 Token）保存到父域中不就行了。没错，我们只需要将 Cookie 的 domain 属性设置为父域的域名（主域名），同时将 Cookie 的 path 属性设置为根路径，这样所有的子域应用就都可以访问到这个 Cookie 了。</p>
<p>不过这要求应用系统的域名需建立在一个共同的主域名之下，如 tieba.baidu.com 和 map.baidu.com，它们都建立在 baidu.com 这个主域名之下，那么它们就可以通过这种方式来实现单点登录。</p>
<p>总结：此种实现方式比较简单，但不支持跨主域名。</p>
<h3 id="实现方式二：认证中心"><a href="#实现方式二：认证中心" class="headerlink" title=" 实现方式二：认证中心 "></a><span id = "2"> 实现方式二：认证中心 </span></h3><p>我们可以部署一个认证中心，认证中心就是一个专门负责处理登录请求的独立的 Web 服务。</p>
<p>用户统一在认证中心进行登录，登录成功后，认证中心记录用户的登录状态，并将 Token 写入 Cookie。（注意这个 Cookie 是认证中心的，应用系统是访问不到的。）</p>
<p>应用系统检查当前请求有没有 Token，如果没有，说明用户在当前系统中尚未登录，那么就将页面跳转至认证中心。由于这个操作会将认证中心的 Cookie 自动带过去，因此，认证中心能够根据 Cookie 知道用户是否已经登录过了。</p>
<p>如果认证中心发现用户尚未登录，则返回登录页面，等待用户登录，如果发现用户已经登录过了，就不会让用户再次登录了，而是会跳转回目标 URL ，并在跳转前生成一个 Token，拼接在目标 URL 的后面，回传给目标应用系统。</p>
<p>应用系统拿到 Token 之后，还需要向认证中心确认下 Token 的合法性，防止用户伪造。确认无误后，应用系统记录用户的登录状态，并将 Token 写入 Cookie，然后给本次访问放行。（注意这个 Cookie 是当前应用系统的，其他应用系统是访问不到的。）当用户再次访问当前应用系统时，就会自动带上这个 Token，应用系统验证 Token 发现用户已登录，于是就不会有认证中心什么事了。</p>
<p>这里顺便介绍两款认证中心的开源实现：</p>
<p>Apereo CAS 是一个企业级单点登录系统，其中 CAS 的意思是”Central Authentication Service“。它最初是耶鲁大学实验室的项目，后来转让给了 JASIG 组织，项目更名为 JASIG CAS，后来该组织并入了Apereo 基金会，项目也随之更名为 Apereo CAS。</p>
<p>XXL-SSO 是一个简易的单点登录系统，由大众点评工程师许雪里个人开发，代码比较简单，没有做安全控制，因而不推荐直接应用在项目中，这里列出来仅供参考。</p>
<p>总结：此种实现方式相对复杂，支持跨域，扩展性好，是单点登录的标准做法。</p>
<h3 id="实现方式三：LocalStorage-跨域"><a href="#实现方式三：LocalStorage-跨域" class="headerlink" title=" 实现方式三：LocalStorage 跨域 "></a><span id = "3"> 实现方式三：LocalStorage 跨域 </span></h3><p>前面，我们说实现单点登录的关键在于，如何让 Session ID（或 Token）在多个域中共享。</p>
<p>父域 Cookie 确实是一种不错的解决方案，但是不支持跨域。那么有没有什么奇淫技巧能够让 Cookie 跨域传递呢？</p>
<p>很遗憾，浏览器对 Cookie 的跨域限制越来越严格。Chrome 浏览器还给 Cookie 新增了一个 SameSite 属性，此举几乎禁止了一切跨域请求的 Cookie 传递（超链接除外），并且只有当使用 HTTPs 协议时，才有可能被允许在 AJAX 跨域请求中接受服务器传来的 Cookie。</p>
<p>不过，在前后端分离的情况下，完全可以不使用 Cookie，我们可以选择将 Session ID （或 Token ）保存到浏览器的 LocalStorage 中，让前端在每次向后端发送请求时，主动将 LocalStorage 的数据传递给服务端。这些都是由前端来控制的，后端需要做的仅仅是在用户登录成功后，将 Session ID （或 Token ）放在响应体中传递给前端。</p>
<p>在这样的场景下，单点登录完全可以在前端实现。前端拿到 Session ID （或 Token ）后，除了将它写入自己的 LocalStorage 中之外，还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。</p>
<p>关键代码如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取 token</span></span><br><span class="line"><span class="keyword">var</span> token = result.data.token;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 动态创建一个不可见的iframe，在iframe中加载一个跨域HTML</span></span><br><span class="line"><span class="keyword">var</span> iframe = document.createElement(<span class="string">&quot;iframe&quot;</span>);</span><br><span class="line">iframe.src = <span class="string">&quot;http://app1.com/localstorage.html&quot;</span>;</span><br><span class="line">document.body.append(iframe);</span><br><span class="line"><span class="comment">// 使用postMessage()方法将token传递给iframe</span></span><br><span class="line">setTimeout(function () &#123;</span><br><span class="line">iframe.contentWindow.postMessage(token, <span class="string">&quot;http://app1.com&quot;</span>);</span><br><span class="line">&#125;, <span class="number">4000</span>);</span><br><span class="line">setTimeout(function () &#123;</span><br><span class="line">iframe.remove();</span><br><span class="line">&#125;, <span class="number">6000</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在这个iframe所加载的HTML中绑定一个事件监听器，当事件被触发时，把接收到的token数据写入localStorage</span></span><br><span class="line">window.addEventListener(<span class="string">&#x27;message&#x27;</span>, function (event) &#123;</span><br><span class="line">localStorage.setItem(<span class="string">&#x27;token&#x27;</span>, event.data)</span><br><span class="line">&#125;, <span class="keyword">false</span>);</span><br></pre></td></tr></table></figure>
<p>前端通过 iframe+postMessage() 方式，将同一份 Token 写入到了多个域下的 LocalStorage 中，前端每次在向后端发送请求之前，都会主动从 LocalStorage 中读取 Token 并在请求中携带，这样就实现了同一份 Token 被多个域所共享。</p>
<p>总结：此种实现方式完全由前端控制，几乎不需要后端参与，同样支持跨域。</p>
<h3 id="补充：域名分级"><a href="#补充：域名分级" class="headerlink" title=" 补充：域名分级 "></a><span id = "4"> 补充：域名分级 </span></h3><p>从专业的角度来说（根据《计算机网络》中的定义），.com、.cn 为一级域名（也称顶级域名），.com.cn、baidu.com 为二级域名，sina.com.cn、tieba.baidu.com 为三级域名，以此类推，N 级域名就是 N-1 级域名的直接子域名。</p>
<p>从使用者的角度来说，一般把可支持独立备案的主域名称作一级域名，如 baidu.com、sina.com.cn 皆可称作一级域名，在主域名下建立的直接子域名称作二级域名，如 tieba.baidu.com 为二级域名。</p>

<div id="gitalk-container"></div>
<script src="https://cdn.bootcss.com/blueimp-md5/2.12.0/js/md5.min.js"></script><link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css"><script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script>

		<script>
		var gitalkConfig = {"clientID":"ee669126f653855f589f","clientSecret":"5669e6253207b59e0db9308d905338e29a6b6cb6","repo":"kaisering.github.io","owner":"kaisering","admin":"kaisering","distractionFreeMode":false};
	    gitalkConfig.id = md5(location.pathname);
		var gitalk = new Gitalk(gitalkConfig);
	    gitalk.render("gitalk-container");
	    </script>
        </div>

        

        

        
            <div class="comment-container">
                <div class="comments-container">
    <div id="comment-anchor"></div>
    
        
    <div id="gitalk-container"></div>
    <script 
            src="//cdn.jsdelivr.net/npm/gitalk/dist/gitalk.min.js"></script>
    <script >

        function loadGitalk() {
            let __gitalk__pathname = decodeURI(location.pathname);
            const __gitalk__pathnameLength = __gitalk__pathname.length;
            const __gitalk__pathnameMaxLength = 50;
            if (__gitalk__pathnameLength > __gitalk__pathnameMaxLength) {
                __gitalk__pathname = __gitalk__pathname.substring(0, __gitalk__pathnameMaxLength - 3) + '...';
            }

            try {
                Gitalk && new Gitalk({
                    clientID: 'ee669126f653855f589f',
                    clientSecret: '5669e6253207b59e0db9308d905338e29a6b6cb6',
                    repo: 'kaisering.github.io',
                    owner: 'kaisering',
                    admin: ['kaisering'],
                    id: __gitalk__pathname,
                    language: 'en'
                }).render('gitalk-container');

            } catch (e) {
                window.Gitalk = null;
            }
        }

        if ('false') {
            const loadGitalkTimeout = setTimeout(() => {
                loadGitalk();
                clearTimeout(loadGitalkTimeout);
            }, 1000);
        } else {
            window.addEventListener('DOMContentLoaded', loadGitalk);
        }
    </script>


    
</div>

            </div>
        
    </div>
</div>


                
            </div>

        </div>

        <div class="page-main-content-bottom">
            <footer class="footer">
    <div class="info-container">
        <div class="copyright-info info-item">
            &copy;
            
              <span>2020</span> -
            
            2021 <i class="fas fa-heart icon-animate"></i> <a href="/">Kaiser</a>
        </div>
        
        <div class="theme-info info-item">
            Powered by <a target="_blank" href="https://hexo.io">Hexo</a> | Theme <a class="theme-version" target="_blank" href="https://github.com/XPoet/hexo-theme-keep">Keep v3.2.0</a>
        </div>
    </div>
</footer>

        </div>
    </div>

    
        <div class="post-tools">
            <div class="post-tools-container">
    <ul class="tools-list">
        <!-- TOC aside toggle -->
        

        <!-- go comment -->
        
            <li class="go-comment">
                <i class="fas fa-comment"></i>
            </li>
        
    </ul>
</div>

        </div>
    

    <div class="right-bottom-side-tools">
        <div class="side-tools-container">
    <ul class="side-tools-list">
        <li class="tools-item tool-font-adjust-plus flex-center">
            <i class="fas fa-search-plus"></i>
        </li>

        <li class="tools-item tool-font-adjust-minus flex-center">
            <i class="fas fa-search-minus"></i>
        </li>

        <li class="tools-item tool-expand-width flex-center">
            <i class="fas fa-arrows-alt-h"></i>
        </li>

        <li class="tools-item tool-dark-light-toggle flex-center">
            <i class="fas fa-moon"></i>
        </li>

        <!-- rss -->
        
            <li class="tools-item rss flex-center">
                <a class="flex-center"
                   href="/atom.xml"
                   target="_blank"
                >
                    <i class="fas fa-rss"></i>
                </a>
            </li>
        

        
            <li class="tools-item tool-scroll-to-top flex-center">
                <i class="fas fa-arrow-up"></i>
            </li>
        

        <li class="tools-item tool-scroll-to-bottom flex-center">
            <i class="fas fa-arrow-down"></i>
        </li>
    </ul>

    <ul class="exposed-tools-list">
        <li class="tools-item tool-toggle-show flex-center">
            <i class="fas fa-cog fa-spin"></i>
        </li>
        
    </ul>
</div>

    </div>

    

    <div class="image-viewer-container">
    <img src="">
</div>


    
        <div class="search-pop-overlay">
    <div class="popup search-popup">
        <div class="search-header">
          <span class="search-input-field-pre">
            <i class="fas fa-keyboard"></i>
          </span>
            <div class="search-input-container">
                <input autocomplete="off"
                       autocorrect="off"
                       autocapitalize="off"
                       placeholder="Search..."
                       spellcheck="false"
                       type="search"
                       class="search-input"
                >
            </div>
            <span class="popup-btn-close">
                <i class="fas fa-times"></i>
            </span>
        </div>
        <div id="search-result">
            <div id="no-result">
                <i class="fas fa-spinner fa-pulse fa-5x fa-fw"></i>
            </div>
        </div>
    </div>
</div>

    

</main>




<script src="/kaisering.github.io/js/utils.js"></script>

<script src="/kaisering.github.io/js/main.js"></script>

<script src="/kaisering.github.io/js/header-shrink.js"></script>

<script src="/kaisering.github.io/js/back2top.js"></script>

<script src="/kaisering.github.io/js/dark-light-toggle.js"></script>



    
<script src="/kaisering.github.io/js/local-search.js"></script>







<div class="post-scripts">
    
</div>



</body>
</html>
